{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:39.580119Z",
     "start_time": "2025-01-21T14:09:39.573580Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1dad802b6500ab83",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.656263Z",
     "start_time": "2025-01-21T14:09:39.699901Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(PosixPath('competitions/cifar-10/train/1.png'), 'frog'),\n",
      " (PosixPath('competitions/cifar-10/train/2.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/3.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/4.png'), 'deer'),\n",
      " (PosixPath('competitions/cifar-10/train/5.png'), 'automobile')]\n",
      "[(PosixPath('competitions/cifar-10/test/1.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/2.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/3.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/4.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/5.png'), 'cat')]\n",
      "50000 300000\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# 目的：通过读取csv文件，得到图片的类别，并将图片路径和类别保存到DataFrame中。\n",
    "\n",
    "DATA_DIR1 = Path(\"./\")\n",
    "DATA_DIR2=Path(\"./competitions/cifar-10/\")\n",
    "\n",
    "train_labels_file = DATA_DIR1 / \"trainLabels.csv\"\n",
    "test_csv_file = DATA_DIR1 / \"sampleSubmission.csv\"  # 测试集模板csv文件\n",
    "train_folder = DATA_DIR2 / \"train/\"\n",
    "test_folder = DATA_DIR2 / \"test/\"\n",
    "\n",
    "# 所有的类别\n",
    "class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n",
    "\n",
    "\n",
    "# filepath:csv文件路径，folder:图片所在文件夹\n",
    "def parse_csv_file(filepath, folder):\n",
    "    result = []\n",
    "    # 读取所有行\n",
    "    # with 语句：用于管理文件的上下文。在 with 块结束时，文件会自动关闭，无需手动调用 f.close()。\n",
    "    with open(filepath, 'r') as f:\n",
    "        # f.readlines()：读取文件的所有行，返回一个列表，每个元素是一行内容。\n",
    "        # 第一行不需要，因为第一行是标题\n",
    "        lines = f.readlines()[1:]\n",
    "    for line in lines:\n",
    "        # strip('\\n')：去除行末的换行符（\\n）。\n",
    "        # split(',')：按','分割字符串，返回一个列表。\n",
    "        image_id, label_str = line.strip('\\n').split(',')\n",
    "        # 得到图片的路径\n",
    "        image_full_path = folder / f\"{image_id}.png\"\n",
    "        # 得到对应图片的路径和分类\n",
    "        result.append((image_full_path, label_str))\n",
    "    return result\n",
    "\n",
    "\n",
    "# 得到训练集和测试集的图片路径和分类\n",
    "train_labels_info = parse_csv_file(train_labels_file, train_folder)\n",
    "test_csv_info = parse_csv_file(test_csv_file, test_folder)\n",
    "\n",
    "#打印\n",
    "import pprint\n",
    "\n",
    "pprint.pprint(train_labels_info[0:5])\n",
    "pprint.pprint(test_csv_info[0:5])\n",
    "print(len(train_labels_info), len(test_csv_info))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8cd5063c39c678cf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.732204Z",
     "start_time": "2025-01-21T14:09:41.658275Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                            filepath       class\n",
      "0  competitions/cifar-10/train/1.png        frog\n",
      "1  competitions/cifar-10/train/2.png       truck\n",
      "2  competitions/cifar-10/train/3.png       truck\n",
      "3  competitions/cifar-10/train/4.png        deer\n",
      "4  competitions/cifar-10/train/5.png  automobile\n",
      "                                filepath       class\n",
      "0  competitions/cifar-10/train/45001.png       horse\n",
      "1  competitions/cifar-10/train/45002.png  automobile\n",
      "2  competitions/cifar-10/train/45003.png        deer\n",
      "3  competitions/cifar-10/train/45004.png  automobile\n",
      "4  competitions/cifar-10/train/45005.png    airplane\n",
      "                           filepath class\n",
      "0  competitions/cifar-10/test/1.png   cat\n",
      "1  competitions/cifar-10/test/2.png   cat\n",
      "2  competitions/cifar-10/test/3.png   cat\n",
      "3  competitions/cifar-10/test/4.png   cat\n",
      "4  competitions/cifar-10/test/5.png   cat\n"
     ]
    }
   ],
   "source": [
    "# 取前45000张图片作为训练集\n",
    "train_df = pd.DataFrame(train_labels_info[0:45000])\n",
    "# 取后5000张图片作为验证集\n",
    "valid_df = pd.DataFrame(train_labels_info[45000:])\n",
    "# 测试集\n",
    "test_df = pd.DataFrame(test_csv_info)\n",
    "\n",
    "# 为 Pandas DataFrame 的列重新命名\n",
    "train_df.columns = ['filepath', 'class']\n",
    "valid_df.columns = ['filepath', 'class']\n",
    "test_df.columns = ['filepath', 'class']\n",
    "\n",
    "print(train_df.head())\n",
    "print(valid_df.head())\n",
    "print(test_df.head())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "af9444393c2debca",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.740688Z",
     "start_time": "2025-01-21T14:09:41.732204Z"
    }
   },
   "outputs": [],
   "source": [
    "from PIL import Image\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torchvision import transforms\n",
    "\n",
    "\n",
    "class Cifar10Dataset(Dataset):\n",
    "    df_map = {\n",
    "        \"train\": train_df,\n",
    "        \"eval\": valid_df,\n",
    "        \"test\": test_df\n",
    "    }\n",
    "    # enumerate(class_names)：遍历 class_names，返回索引和类别名称的元组\n",
    "    # 将类别名称映射为索引，通常用于将标签转换为模型可以处理的数值。\n",
    "    label_to_idx = {\n",
    "        label: idx for idx, label in enumerate(class_names)\n",
    "    }\n",
    "    # 将索引映射为类别名称，通常用于将模型的输出（索引）转换回可读的类别名称\n",
    "    idx_to_label = {\n",
    "        idx: label for idx, label in enumerate(class_names)\n",
    "    }\n",
    "\n",
    "    def __init__(self, mode, transform=None):\n",
    "        # 获取对应模式的df，不同字符串对应不同模式\n",
    "        self.df = self.df_map.get(mode, None)\n",
    "        if self.df is None:\n",
    "            raise ValueError(f\"Invalid mode: {mode}\")\n",
    "        self.transform = transform\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # 获取图片路径和标签\n",
    "        img_path, label = self.df.iloc[index]\n",
    "        # 打开一张图片并将其转换为 RGB 格式\n",
    "        img = Image.open(img_path).convert('RGB')  # 确保图片具有三个通道\n",
    "        # 应用数据增强\n",
    "        img = self.transform(img)\n",
    "        # label 转换为 idx\n",
    "        label = self.label_to_idx[label]\n",
    "        return img, label\n",
    "\n",
    "    def __len__(self):\n",
    "        # 返回df的行数,样本数\n",
    "        return self.df.shape[0]\n",
    "\n",
    "\n",
    "IMAGE_SIZE = 32\n",
    "mean, std = [0.4914, 0.4822, 0.4465], [0.247, 0.243, 0.261]\n",
    "\n",
    "#数据增强\n",
    "# ToTensor还将图像的维度从[height, width, channels]转换为[channels, height, width]。\n",
    "transforms_train = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),  # 缩放\n",
    "    transforms.RandomRotation(40),  # 随机旋转\n",
    "    transforms.RandomHorizontalFlip(),  # 随机水平翻转\n",
    "    transforms.ToTensor(),  # 转换为Tensor\n",
    "    transforms.Normalize(mean, std)  # 标准化\n",
    "])\n",
    "\n",
    "transforms_eval = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize(mean, std)\n",
    "])\n",
    "\n",
    "train_ds = Cifar10Dataset(mode=\"train\", transform=transforms_train)\n",
    "eval_ds = Cifar10Dataset(mode=\"eval\", transform=transforms_eval)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d17ef4a56f0b6ae1",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.748174Z",
     "start_time": "2025-01-21T14:09:41.742701Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([3, 32, 32])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[0][0].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "161282a4e612afcd",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.753134Z",
     "start_time": "2025-01-21T14:09:41.749187Z"
    }
   },
   "outputs": [],
   "source": [
    "batch_size = 64\n",
    "\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size,\n",
    "                          shuffle=True)\n",
    "eval_loader = DataLoader(eval_ds, batch_size=batch_size,\n",
    "                         shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20b5b31416450980",
   "metadata": {},
   "source": [
    "## 模型定义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b73daeeb83b4b527",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.760182Z",
     "start_time": "2025-01-21T14:09:41.754138Z"
    }
   },
   "outputs": [],
   "source": [
    "class Resdiual(nn.Module):\n",
    "    \"\"\"\n",
    "    浅层的残差块，无bottleneck（性能限制\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels,\n",
    "                 use_1x1conv=False, stride=1):\n",
    "        \"\"\"\n",
    "        残差块\n",
    "        params filters: 过滤器数目，决定输出通道\n",
    "        params use_1x1conv: 是否使用 1x1 卷积，此时 stride=2，进行降采样\n",
    "        params strides: 步长，默认为1，当降采样的时候设置为2\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        # [1+(n+2-3)]//2=n//2\n",
    "        # 即该方法可能会降采样\n",
    "        self.conv1 = nn.Conv2d(\n",
    "            in_channels=input_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=stride,\n",
    "            padding=1\n",
    "        )\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        self.conv2 = nn.Conv2d(\n",
    "            in_channels=output_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=1,\n",
    "            padding=1\n",
    "        )\n",
    "        if use_1x1conv:\n",
    "            # skip connection 的 1x1 卷积，用于改变通道数和降采样，使得最终可以做残差连接\n",
    "            # [1+n-1]//s=n//s\n",
    "            self.conv_sc = nn.Conv2d(\n",
    "                in_channels=input_channels,\n",
    "                out_channels=output_channels,\n",
    "                kernel_size=1,\n",
    "                stride=stride\n",
    "            )\n",
    "        else:\n",
    "            self.conv_sc = None\n",
    "\n",
    "        self.bn1 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "        self.bn2 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        flow = F.relu(self.bn1(self.conv1(inputs)))  # 卷积->BN->ReLU\n",
    "        flow = self.bn2(self.conv2(flow))  # 卷积->BN\n",
    "        if self.conv_sc:\n",
    "            inputs = self.conv_sc(inputs)  # 1x1卷积，改变通道数和降采样\n",
    "        # 残差连接->ReLU，必须保证flow和inputs的shape相同\n",
    "        return F.relu(flow + inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fc16467b5391fd55",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.766476Z",
     "start_time": "2025-01-21T14:09:41.761185Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResdiualBlock(nn.Module):\n",
    "    \"\"\"\n",
    "    若干个 Resdiual 模块堆叠在一起，\n",
    "    通常在第一个模块给 skip connection 使用 1x1conv with stride=2\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels, num, is_first=False):\n",
    "        \"\"\"\n",
    "        params filters: 过滤器数目\n",
    "        params num: 堆叠几个 Resdiual 模块\n",
    "        params is_first: 是不是第一个block。 最上面一层 Resdiual 的 stride=1,is_first=False,图像尺寸减半，False图像尺寸不变\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential()  # 用于存放 Resdiual 模块\n",
    "        # append() 等价于 add_module()\n",
    "        self.model.append(Resdiual(\n",
    "            input_channels=input_channels,\n",
    "            output_channels=output_channels,\n",
    "            use_1x1conv=not is_first,\n",
    "            stride=1 if is_first else 2\n",
    "        ))  # 第一个 Resdiual 模块，负责通道翻倍,图像的尺寸减半\n",
    "        for _ in range(1, num):\n",
    "            # 堆叠 num 个 Resdiual 模块\n",
    "            self.model.append(Resdiual(\n",
    "                input_channels=output_channels,\n",
    "                output_channels=output_channels,\n",
    "                use_1x1conv=False,\n",
    "                stride=1\n",
    "            ))\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8128761d5d6c0e1d",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.773688Z",
     "start_time": "2025-01-21T14:09:41.767479Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResNetCifar10(nn.Module):\n",
    "    def __init__(self, n=3, num_classes=10):\n",
    "        \"\"\"\n",
    "        params units: 预测类别的数目\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential(\n",
    "            # conv1\n",
    "            nn.Conv2d(in_channels=3, out_channels=16,\n",
    "                      kernel_size=3, stride=1),\n",
    "            nn.BatchNorm2d(16, momentum=0.9, eps=1e-5),\n",
    "            nn.ReLU(),\n",
    "            # conv2_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=16,\n",
    "                          num=2 * n, is_first=True),\n",
    "            # conv3_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=32,\n",
    "                          num=2 * n),\n",
    "            # conv4_x\n",
    "            ResdiualBlock(input_channels=32, output_channels=64,\n",
    "                          num=2 * n),\n",
    "            # 全局平均池化层\n",
    "            #无论输入图片大小，输出都是1x1，把width和height压缩为1\n",
    "            nn.AdaptiveAvgPool2d((1, 1)),\n",
    "            # 全连接层\n",
    "            nn.Flatten(),  # 64*1*1 -> 64\n",
    "            # 输出层\n",
    "            nn.Linear(in_features=64, out_features=num_classes)\n",
    "        )\n",
    "        self.init_weights()\n",
    "\n",
    "    def init_weights(self):\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
    "                # Kaiming 初始化（也称为 He 初始化）是为深度神经网络设计的一种初始化方法，特别适用于 ReLU 激活函数。\n",
    "                nn.init.kaiming_uniform_(m.weight)\n",
    "                if m.bias is not None:\n",
    "                    nn.init.zeros_(m.bias)\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bb519b4756d62090",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.777720Z",
     "start_time": "2025-01-21T14:09:41.774691Z"
    }
   },
   "outputs": [],
   "source": [
    "# for key, value in ResNetCifar10(len(class_names)).named_parameters():\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b294d25e7f6287d0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.825513Z",
     "start_time": "2025-01-21T14:09:41.779726Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 1929546\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in ResNetCifar10(len(class_names)).parameters() if p.requires_grad)\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76ca49d0dca74376",
   "metadata": {},
   "source": [
    "## 模型训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "632d31ce33563b0c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.909287Z",
     "start_time": "2025-01-21T14:09:41.826518Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()  # 装饰器，禁止梯度计算\n",
    "def evaluate(model, data_loader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in data_loader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        # 前向传播\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        # tensor.item() 获取tensor的数值，loss是只有一个元素的tensor\n",
    "        loss_list.append(loss.item())\n",
    "\n",
    "        # 预测\n",
    "        preds = logits.argmax(axis=-1)  # 预测类别\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())  # tensor转numpy，再转list\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "\n",
    "    acc = accuracy_score(label_list, pred_list)  # 计算准确率\n",
    "    return np.mean(loss_list), acc  # # 返回验证集平均损失和准确率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "28c2ba49f4ff546",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.915505Z",
     "start_time": "2025-01-21T14:09:41.910290Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=500, save_best_only=True):\n",
    "        self.save_dir = save_dir  # 保存路径\n",
    "        self.save_step = save_step  # 保存步数\n",
    "        self.save_best_only = save_best_only  # 是否只保存最好的模型\n",
    "        self.best_metric = -1  # 最好的指标，指标不可能为负数，所以初始化为-1\n",
    "        # 创建保存路径\n",
    "        if not os.path.exists(self.save_dir):  # 如果不存在保存路径，则创建\n",
    "            os.makedirs(self.save_dir)\n",
    "\n",
    "    # 对象被调用时：当你将对象像函数一样调用时，Python 会自动调用 __call__ 方法。\n",
    "    # state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "    # metric 是指标，可以是验证集的准确率，也可以是其他指标\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return  # 不是保存步数，则直接返回\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None  # 必须传入metric\n",
    "            if metric >= self.best_metric:  # 如果当前指标大于最好的指标\n",
    "                # save checkpoint\n",
    "                # 保存最好的模型，覆盖之前的模型，不保存step，只保存state_dict，即模型参数，不保存优化器参数\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"08_resnet.ckpt\"))\n",
    "                self.best_metric = metric  # 更新最好的指标\n",
    "        else:\n",
    "            # 保存模型\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "            # 保存每个step的模型，不覆盖之前的模型，保存step，保存state_dict，即模型参数，不保存优化器参数\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cd4d61586ad24eb5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.921575Z",
     "start_time": "2025-01-21T14:09:41.916508Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        self.patience = patience  # 多少个step没有提升就停止训练\n",
    "        self.min_delta = min_delta  # 最小的提升幅度\n",
    "        self.best_metric = -1  # 记录的最好的指标\n",
    "        self.counter = 0  # 计数器，记录连续多少个step没有提升\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:  # 如果指标提升了\n",
    "            self.best_metric = metric  # 更新最好的指标\n",
    "            self.counter = 0  # 计数器清零\n",
    "        else:\n",
    "            self.counter += 1  # 计数器加一\n",
    "\n",
    "    @property  # 使用@property装饰器，使得 对象.early_stop可以调用，不需要()\n",
    "    def early_stop(self):\n",
    "        # 如果计数器大于等于patience，则返回True，停止训练\n",
    "        return self.counter >= self.patience"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "725cf8a521379801",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.930997Z",
     "start_time": "2025-01-21T14:09:41.922578Z"
    }
   },
   "outputs": [],
   "source": [
    "def training(model,\n",
    "             train_loader,\n",
    "             val_loader,\n",
    "             epoch,\n",
    "             loss_fct,\n",
    "             optimizer,\n",
    "             save_ckpt_callback=None,\n",
    "             early_stop_callback=None,\n",
    "             eval_step=500,\n",
    "             ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0  # 全局步数\n",
    "    model.train()  # 训练模式\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "\n",
    "                # 前向传播\n",
    "                logits = model(datas)\n",
    "                loss = loss_fct(logits, labels)  # 训练集损失\n",
    "                preds = logits.argmax(axis=-1)  # 预测类别\n",
    "\n",
    "                # 反向传播\n",
    "                optimizer.zero_grad()  # 梯度清零\n",
    "                loss.backward()  # 反向传播\n",
    "                optimizer.step()  # 优化器更新参数\n",
    "\n",
    "                # 计算准确率\n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
    "                loss = loss.cpu().item()\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss,\n",
    "                    \"acc\": acc,\n",
    "                    \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 评估\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()  # 评估模式\n",
    "                    # 验证集损失和准确率\n",
    "                    val_loss, val_acc = evaluate(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss,\n",
    "                        \"acc\": val_acc,\n",
    "                        \"step\": global_step\n",
    "                    })\n",
    "                    model.train()  # 训练模式\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        # model.state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), val_acc)\n",
    "                        # 保存最好的模型，覆盖之前的模型，保存step，保存state_dict,通过metric判断是否保存最好的模型\n",
    "\n",
    "                    # 3. 早停 early stopping\n",
    "                    if early_stop_callback is not None:\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        early_stop_callback(val_acc)\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict  # 早停，返回记录字典 record_dict\n",
    "\n",
    "                # 更新进度条和全局步数\n",
    "                pbar.update(1)  # 更新进度条\n",
    "                global_step += 1  # 全局步数加一\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict  # 训练结束，返回记录字典 record_dict\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "22bf1e82c9715088",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.979444Z",
     "start_time": "2025-01-21T14:09:41.931999Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "epoch = 100\n",
    "\n",
    "model = ResNetCifar10(len(class_names))  # 定义模型\n",
    "model = model.to(device)  # 将模型移到GPU上\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "\n",
    "# 2. 定义优化器 采用 AdamW\n",
    "optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, betas=(0.9, 0.999), weight_decay=0.01)\n",
    "\n",
    "# 3.save model checkpoint\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(save_dir=\"checkpoints\", save_step=len(train_loader), save_best_only=True)\n",
    "\n",
    "# 4. early stopping\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "5335f62fe5e4977f",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.118493Z",
     "start_time": "2025-01-21T14:09:41.980446Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 35%|███▌      | 24640/70400 [29:53<55:30, 13.74it/s, epoch=34]  "
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 35 / global_step 24640\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "\n",
    "# 训练过程\n",
    "record_dict = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    eval_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "1a790e5f4635d5e8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.120498Z",
     "start_time": "2025-01-21T14:10:19.119497Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def plot_record_curves(record_dict, sample_step=500):\n",
    "    # .set_index(\"step\") 将 step 列设置为 DataFrame 的索引\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    last_step = train_df.index[-1]  # 最后一步的步数\n",
    "\n",
    "    # print(train_df)\n",
    "    # print(val_df)\n",
    "\n",
    "    # 画图 \n",
    "    fig_num = len(train_df.columns)  # 画两张图,分别是损失和准确率\n",
    "\n",
    "    # plt.subplots：用于创建一个包含多个子图的图形窗口。\n",
    "    # 1：表示子图的行数为 1。\n",
    "    # fig_num：表示子图的列数，即子图的数量。\n",
    "    # figsize=(5 * fig_num, 5)：设置整个图形窗口的大小，宽度为 5 * fig_num，高度为 5。\n",
    "    # fig：返回的图形对象（Figure），用于操作整个图形窗口。\n",
    "    # axs：返回的子图对象（Axes 或 Axes 数组），用于操作每个子图。\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        # train_df.index 是 x 轴数据（通常是 step）。\n",
    "        # train_df[item] 是 y 轴数据（当前指标的值）。\n",
    "        axs[idx].plot(train_df.index, train_df[item], label=\"train:\" + item)\n",
    "        # val_df.index 是 x 轴数据。\n",
    "        # val_df[item] 是 y 轴数据。\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=\"val:\" + item)\n",
    "        axs[idx].grid()  # 显示网格\n",
    "        axs[idx].legend()  # 显示图例\n",
    "        axs[idx].set_xticks(range(0, train_df.index[-1] + 1, 5000))  # 设置x轴刻度\n",
    "        axs[idx].set_xticklabels(map(lambda x: f\"{x // 1000}k\", range(0, last_step + 1, 5000)))  # 设置x轴标签\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_record_curves(record_dict)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d9729d15a0d01e8",
   "metadata": {},
   "source": [
    "## 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "3195e10a60c29806",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.121498Z",
     "start_time": "2025-01-21T14:10:19.121498Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test loss: 0.3871, Test acc: 0.8708\n"
     ]
    }
   ],
   "source": [
    "# 加载最好的模型\n",
    "# torch.load：加载保存的模型权重或整个模型。\n",
    "# \"checkpoints/best.ckpt\"：模型权重文件路径。\n",
    "# weights_only=True：仅加载模型的权重，而不是整个模型（包括结构和参数）。这是 PyTorch 2.1 引入的新特性，用于增强安全性。\n",
    "# map_location=device：将模型加载到当前设备（GPU或CPU）。\n",
    "model.load_state_dict(torch.load(\"checkpoints/08_resnet.ckpt\", weights_only=True, map_location=device))  # 加载最好的模型\n",
    "\n",
    "model.eval()  # 评估模式\n",
    "loss, acc = evaluate(model, eval_loader, loss_fct)\n",
    "print(f\"Test loss: {loss:.4f}, Test acc: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
